/**
 ****************************************************************************
 * Copyright (C) Marcelo F. Ochoa. All rights reserved.                      *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included  with this distribution in *
 * the LICENSE file.                                                         *
 */

package com.prism.oracle;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import oracle.jdbc.xa.OracleXid;
import oracle.jdbc.xa.client.OracleXADataSource;
import oracle.jdbc.xa.client.OracleXAResource;

import org.jconfig.Configuration;

import com.prism.utils.Block;

/**
 * This class is a Singleton that provides access to one or many connection pools defined in a Property file. A client gets
 * access to the single instance through the static getInstance() Need a java enabled 8.1.6+ database to run.
 * TXBranchManager manages physical connection to the database
 * Every physical connection can manage multiple logical connection demarcated by XA compliant calls.
 */
public class TXBranchManager implements java.lang.Runnable {
    // Private attributes used with TX connections
    // shared components
    private static boolean bContinue = true;
    private static Hashtable connectionInUse = null;
    private static java.lang.Thread branchThread = null;
    private static int connectionTimeOut = 0;
    private static TXBranchManager instance = null;

    /**
     * Problem: Transaction which shares a physical connection must be wait for the physical resource until it is free.
     * The amount of time betwen [start|resume] calls and suspend is less than
     * to use another physical connection then the request is bloqued until the
     * stored procedured is executed and the page is returned.
     */
    private static Block blocker = null;
    // member components
    private XAResource oxar = null;
    private Xid xid = null;
    private int gID = 0;
    // logical connection
    private TXConnection pConn = null;

    /** Singleton */
    public static synchronized TXBranchManager getInstance(Configuration props) throws Exception {
        if (instance == null) {
            //System.out.println("Initializing Transaction manager...");
            instance = new TXBranchManager(props);
        }
        return instance;
    }

    /** A private constructor since this is a Singleton */
    private TXBranchManager(Configuration props) throws Exception {
        init(props);
    }

    public synchronized void init(Configuration props) {
        connectionInUse = new Hashtable();
        try {
            connectionTimeOut = (new Integer(props.getProperty("timeout","600","Manager"))).intValue();
        } catch (Exception e) { connectionTimeOut = 600; };
        branchThread = new Thread(this);
        branchThread.start();
        blocker = new Block();
    }

    /** replace deprecated method for jdk 1.2 Stop the tread by set to false boolean var of while loop in method Run */
    public void stopManager() {
        bContinue = false;
    }

    /**
     * this method inherit from Runnable interface check every 1 second for unused connection (in BusyList)
     * if the connection is unused for {Manager.timeout} seconds - close it (BusyList)
     */
    public void run() {
        try {
            //System.out.println("run: Thread  started...");
            while (bContinue) {
                synchronized(connectionInUse) {
                    Enumeration en = connectionInUse.keys();
                    while (en.hasMoreElements()) {
                        String key = (String)en.nextElement();
                        TXConnection txConn = (TXConnection)connectionInUse.get(key);
                        //System.out.println("run: "+key+" counter "+txConn.counter);
                        if (txConn.counter > 0)
                            txConn.counter--;
                        else {
                            //System.out.println("run: Remove connection from connectionInUse"+key);
                            try {
                                txConn.release();
                            } catch (SQLException sqe) { }; // Ignore errors
                            connectionInUse.remove(key);
                        }
                    }
                }
                branchThread.sleep(1000); // Wait 1 second
            }
            //System.out.println("run: Thread  finished...");
        } catch (Exception e) {
            System.out.println("Can't start the TX Thread or Thread interrup");
            e.printStackTrace();
        }
    }

    /** Constructor gbID, global branch identifier */
    public TXBranchManager(int gbID) {
        gID = gbID;
    }

    /**
     * start a new transaction - gets physical connection, looking at connectionInUse,
     * if there is a physical connection get them and use it. if not, creates one and stores it for future use of him or other
     * transaction - locks this physical connection because it couldn't shared by other transaction at the same time.
     * - creates an transaction id to identified this transaction branch - start the transaction branch
     * - reset counter of innactivity on this physical connection - return the physical connection to be use by DB Prism
     */
    public Connection start(String connectString, String txName, String usr, String pass) throws XAException, SQLException {
        try {
            String connectionIdKey = key(connectString, usr, pass);
            synchronized(connectionInUse) {
                pConn = (TXConnection)connectionInUse.get(connectionIdKey);
                if (pConn == null) {
                    // There isn't DataSource for this connection create one
                    // and create a physical connection for it.
                    pConn = new TXConnection(connectString, usr, pass);
                    connectionInUse.put(connectionIdKey, pConn);
                }
                // block physical connection until it will released by release/commit/rollback
                lock();
            }
            // Get XA resource handle
            oxar = pConn.pc.getXAResource();
            xid = createXid(gID, gID);
            // Start a transaction branch
            oxar.start(xid, XAResource.TMNOFLAGS);
            pConn.counter = connectionTimeOut;
            //System.out.println("Start transaction "+oxar+":"+xid);
            //System.out.println("Start transaction "+pConn);
            return pConn.conn;
        } catch (XAException xae) {
            throw xae;
        } finally {
            if (pConn != null)
                // unlock physical connection
                    unlock();
        }
    }

    /**
     * First lock physical connection Resumes this transaction, if there is an exception unlock physical connection
     * and throw the exception.
     */
    public Connection resume() throws XAException {
        // block physical connection until it will released by suspend/commit/rollback
        lock();
        // Resume the current global transaction
        try {
            ((OracleXAResource)oxar).resume(xid);
            // restart innactivity counter
            pConn.counter = connectionTimeOut;
            //System.out.println("Resume transaction "+oxar+":"+xid);
            //System.out.println("Resume transaction "+pConn);
            return pConn.conn;
        } catch (XAException xae) {
            throw xae;
        } finally {
            // unlock physical connection
            unlock();
        }
    }

    /** Suspends this transaction. finally unlock physical connection */
    public void suspend() throws XAException {
        // Suspend the global transaction
        try {
            ((OracleXAResource)oxar).suspend(xid);
        } catch (XAException xae) {
            throw xae;
        } finally {
            // unlock physical connection
            unlock();
        }
        //System.out.println("Suspend transaction "+oxar+":"+xid);
        //System.out.println("Suspend transaction "+pConn);
    }

    /** Ends this transaction and rollback it. finally unlock physical connection */
    public void rollback() throws XAException {
        // End the branch
        // Do a 1 phase rollback
        try {
            oxar.end(xid, XAResource.TMFAIL);
            oxar.rollback(xid);
        } catch (XAException xae) {
            throw xae;
        } finally {
            // unlock physical connection
            unlock();
        }
        //System.out.println("Rollback transaction "+oxar+":"+xid);
        //System.out.println("Rollback transaction "+pConn);
    }

    /** Ends this transaction and commit it. finally unlock physical connection */
    public void commit() throws XAException {
        // End the branch
        // Do a 1 phase rollback
        try {
            oxar.end(xid, XAResource.TMSUCCESS);
            oxar.commit(xid, true);
        } catch (XAException xae) {
            throw xae;
        } finally {
            // unlock physical connection
            unlock();
        }
        //System.out.println("Commit transaction "+oxar+":"+xid);
        //System.out.println("Commit transaction "+pConn);
    }

    /**
     * Closes all physical connections and free resources and locks
     * This method is called only from JTADBPrismConnectionCacheImpl class at shutdown time.
     */
    public void release() throws SQLException {
        stopManager();
        synchronized(connectionInUse) {
            Enumeration en = connectionInUse.elements();
            while (en.hasMoreElements()) {
                TXConnection txConn = (TXConnection)en.nextElement();
                try {
                    txConn.release();
                    // removes locks for this physical connection
                    blocker.unlock(txConn);
                } catch (Exception e) { }; // Ignore errors
            }
            connectionInUse.clear();
        }
    }

    public void lock() {
        // block physical connection called from start/resume methods
        this.blocker.lock(pConn);
    }

    public void unlock() {
        // removes locks for this physical connection called from suspend/commit/rollback
        this.blocker.unlock(pConn);
    }

    /** Returns a transaction ID identifier used for coordinating the branches of a distributed transaction. */
    Xid createXid(int gd, int bd) throws XAException {
        byte[] gid = new byte[1]; gid[0] = (byte)gd;
        byte[] bid = new byte[1]; bid[0] = (byte)bd;
        byte[] gtrid = new byte[64];
        byte[] bqual = new byte[64];
        System.arraycopy(gid, 0, gtrid, 0, 1);
        System.arraycopy(bid, 0, bqual, 0, 1);
        Xid xid = new OracleXid(0x1234, gtrid, bqual);
        return xid;
    }

    /**
     * key string used on connectionInUse hashTable to identified a physical connection
     * any physical connection is identified by his connect string (database resource)
     * and his username/passwd in uppercase because in Oracle databases are insensitive
     */
    String key(String connectString, String usr, String pass) {
        // username and password are case insentive on Oracle databases
        return connectString + "@" + usr.toUpperCase() + "/" + pass.toUpperCase();
    }

    /**
     * Inner class TXConnection. Stores the physical connection information
     * This physical connection is shared by many logical connecions.
     * Physical connections are stored in list of connectionInUse to stay opened betwen differents calls.
     * These physical connection persist opened until Manager.timeout parameter of prism.properties file.
     */
    private class TXConnection {
        // count every secon for unused logical connections
        int counter = connectionTimeOut;
        // XADataSource instance
        OracleXADataSource oxds = null;
        // XA connection
        XAConnection pc = null;
        // logical connection
        Connection conn = null;

        /** Constructor */
        TXConnection(String connectString, String usr, String pass) throws SQLException {
            //System.out.println("Creating physical connection for "+key(connectString,usr,pass));
            // Create a XADataSource instance
            oxds = new OracleXADataSource();
            oxds.setURL(connectString);
            oxds.setUser(usr.toUpperCase());
            oxds.setPassword(pass.toUpperCase());
            // get a XA connection
            pc = oxds.getXAConnection();
            // Get a logical connection
            conn = pc.getConnection();
            //System.out.println("Physical connection created "+conn);
        }

        void release() throws SQLException {
            //System.out.println("Physical connection closed "+conn);
            conn.close();
            conn = null;
            pc.close();
            pc = null;
        }
    } // End inher class
}
